查看原文
其他

SpringBoot 开发 Web 系统,快速入门指南!

志哥聊技术 潘志的研发笔记
2024-08-30

01、背景介绍

在之前的文章中,我们简单的介绍了 SpringBoot 项目的创建过程,了解了 Spring Boot 开箱即用的特性,本篇文章接着上篇的内容继续介绍 Spring Boot 用于 web 工程开发时的其它特性。

废话不多说了,上代码!

02、应用实践

当将 SpringBoot 框架用于传统的 web 项目开发时,通常分为以下三个过程来实现。

  • 第一步:连接数据库,实现对表进行 CRUD 操作
  • 第二步:引入模板引擎来开发页面
  • 第三步:使用一些常见的 web 特性来满足其它的功能开发

最后源码目录结构如下!

springboot-hello
├── src
│   └── main
│       ├── java
│          ├── com
│             ├── example
│                ├── springboot
│                   ├── Application.java
│                   ├── entity
│                      ├── User.java
│                   ├── service
│                      ├── UserService.java
│                   ├── web
│                      ├── UserController
│       └── resources
│           ├── application.properties
│           ├── templates
│           └─── index.html
└── pom.xml

下面我们依次来看看相关的实践过程。

2.1、数据库操作

这里我们以 Mysql 数据库为例,采用 Spring 的 JdbcTemplate 模板来操作数据的的增删改查,过程如下。

2.1.1、准备数据库表

先创建tb_user表,包含属性id、name、age,可以通过执行下面的建表语句。

CREATE TABLE `tb_user` (
  `id` bigint(20unsigned NOT NULL,
  `name` varchar(30DEFAULT NULL,
  `age` int(11DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.1.2、添加相关依赖包

在我们访问数据库的时候,需要先配置一个数据库驱动包,通常采用 JDBC 方式方式访问,需要在pom.xml中引入相关依赖包。

<!--spring jdbc-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--mysql 驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

2.1.3、添加数据源配置

与此同时,还需要在application.properties文件中配置相关的数据源访问地址。

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

2.1.4、编写领域对象

根据数据库中创建的 User 表,创建对应的领域对象。

package com.example.springboot.entity;

public class User {

    /**
     * 用户ID
     */

    private Long id;

    /**
     * 用户名称
     */

    private String name;

    /**
     * 用户年龄
     */

    private Integer age;

    // set、get方法等...
}

2.1.5、编写数据访问对象

通过JdbcTemplate实现对tb_user表中的数据访问操作。

package com.example.springboot.service;

@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 查询用户
     * @return
     */

    public List<User> getAll() {
        List<User> users = jdbcTemplate.query("select id, name, age from tb_user", (resultSet, i) -> {
            User user = new User();
            user.setId(resultSet.getLong("id"));
            user.setName(resultSet.getString("name"));
            user.setAge(resultSet.getInt("age"));
            return user;
        });
        return users;
    }

    /**
     * 通过ID查询用户
     * @param id
     * @return
     */

    public User getById(Long id) {
        User target = jdbcTemplate.queryForObject("select id, name, age from tb_user where id = ?"new BeanPropertyRowMapper<User>(User.class),id);;
        return target;
    }

    /**
     * 创建用户
     * @param entity
     * @return
     */

    public int create(User entity){
        return jdbcTemplate.update("insert into tb_user(id, name, age) values(?, ?, ?)", entity.getId(), entity.getName(), entity.getAge());
    }

    /**
     * 修改用户
     * @param entity
     * @return
     */

    public int updateById(User entity){
        return jdbcTemplate.update("update tb_user set  name = ?, age = ? where id = ? ", entity.getName(), entity.getAge(), entity.getId());
    }


    /**
     * 删除用户
     * @param id
     * @return
     */

    public int deleteById(Long id) {
        return jdbcTemplate.update("delete from tb_user where id = ?", id);
    }
}

2.1.6、编写单元测试用例

src/test/java目录下,编写单元测试用例,验证代码中的增、删、改、查操作的正确性,包名与主目录中保持一致。

package com.example.springboot;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest 
{

    @Autowired
    private UserService userService;

    @Test
    public void test(){
        // 插入5条数据
        userService.create(new User(1L"张三"20));
        userService.create(new User(2L"李四"21));
        userService.create(new User(3L"王五"22));

        // 查询全部数据
        List<User> dbList1 = userService.getAll();
        System.out.println("第一次全量查询结果:" + dbList1.toString());

        // 修改数据
        userService.updateById(new User(2L"赵六"21));

        // 查询指定数据
        User updateObj = userService.getById(2l);
        System.out.println("查询[id=2]结果:" + updateObj.toString());

        // 删除数据
        userService.deleteById(2L);
        userService.deleteById(3L);

        // 查询全部数据
        List<User> dbList2 = userService.getAll();
        System.out.println("第二次全量查询结果:" + dbList2.toString());
    }
}

单元测试,运行后的输出结果:

第一次全量查询结果:[User{id=1, name='张三', age=20}, User{id=2, name='李四', age=21}, User{id=3, name='王五', age=22}]
查询[id=2]结果:User{id=2, name='赵六', age=21}
第二次全量查询结果:[User{id=1, name='张三', age=20}]

此时操作数据库中的表数据,已经正常流通了。

上面介绍的JdbcTemplate只是最基本的几个操作,更多其他数据访问操作的使用可以参考:JdbcTemplate API

2.2、Thymeleaf 模板

在传统的 Java web 工程中,通常会采用 JSP 来编写页面并进行数据展示。而在 Spring Boot 框架中,推荐使用 Thymeleaf 模板引擎来开发 Web 页面。

Thymeleaf 是一款用于渲染 XML/XHTML/HTML5 内容的模板引擎,与 JSP、Velocity、FreeMarker 等类似,都可以轻易的与 Spring MVC 等 Web 框架进行集成作为 Web 应用的模板引擎。

下面我们一起来看下简单的页面集成应用。

2.2.1、添加相关依赖包

在 SpringBoot 项目中使用 Thymeleaf 时,只需要添加所需的模板引擎模块依赖包即可,内容如下。

<!--thymeleaf模板引擎-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2.2.2、添加相关的配置参数

与此同时,还需要在application.properties文件中配置 thymeleaf 模版扫描路径,比如如下配置。

# thymelea模板配置
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true

其中spring.thymeleaf.prefix就是模板引擎扫描的路径。

2.2.3、创建页面模板

根据上一步映射的模板路径, 在模板路径src/main/resources/templates下新建模板文件index.html,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Home Page</title>
</head>
<body>
<h1>Hello !</h1>
<table>
    <thead>
    <tr>
        <th>用户ID</td>
        <th>用户名称</td>
        <th>用户年龄</td>
    </tr>
    </thead>
    <tbody>
    <tr th:each="prod:${allUsers}">
        <td th:text="${prod.id}">100</td>
        <td th:text="${prod.name}">张三丰</td>
        <td th:text="${prod.age}">99</td>
    </tr>
    </tbody>
</table>
</body>
</html>

2.2.4、编写页面请求对象

最后编写一个页面请求对象,用来处理路径的请求,将数据渲染到index页面上,具体实现如下:

@Controller
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/")
    public String index(ModelMap map) {
        // 查询所有的用户
        List<User> users =  userService.getAll();
        map.addAttribute("allUsers", users);
        return "index";
    }
}

将上文中的三条数据插入到数据库,以便展示。

2.2.5、测试页面展示情况

最后将服务启动,在浏览器发起请求,地址为http://localhost:8080/,展示结果如下:

说明页面渲染正常,符合预期效果。

更多 Thymeleaf 的页面语法,可以访问 Thymeleaf 的官方文档来深入学习使用。

2.3、web 基本特性

除了以上功能,SpringBoot 还有几个常用特性功能,比如 SpringMVC 中的接口开发、过滤器、拦截器、aop 代理、异常处理等。

下面,我们一起简要的看看相关特性的用法。

2.3.1、接口开发

当与其它项目对接的时候,通常会采用 json 数据格式进行请求和返回,在传统的 SpringMVC 项目中,我们通常需要在每个接口方法上加@ResponseBody注解,以便数据以 json 格式返回给用户。

在 Spring Boot 框架中,我们只需要在接口类上添加@RestController注解,即可实现@Controller@ResponseBody一样的效果。

示例如下。

@RestController
public class ApiController {

    @Autowired
    private UserService userService;

    @GetMapping("/getUsers")
    public List<User> getUsers() {
        // 查询所有的用户
        System.out.println("收到查询用户的请求");
        List<User> users =  userService.getAll();
        return users;
    }
}

将服务启动,访问http://localhost:8080/getUsers,看看控制台输出结果。

可以看到,与预期一致。

如果是页面开发,只要使用@Controller注解即可,以免无法渲染数据。

2.3.2、过滤器

过滤器在 web 项目开发过程中经常会用到,比如用于收集调用日志、排除有 XSS 威胁的字符等,过滤器本质不属于 SpringBoot 自带的功能,而是 Servlet 提供的功能,SpringBoot 对此做了集成管理,实现方式也很简单。

首先创建一个过滤器实现类,示例如下。

public class LogFilter implements Filter {


    @Override
    public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        System.out.println("日志过滤器,request url :"+request.getRequestURI());
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

然后将过滤器注册到 SpringBoot 中,示例如下。

@Configuration
public class FilterConfig {

    /**
     * 添加过滤器
     * @return
     */

    @Bean
    public FilterRegistrationBean helloFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setName("logFilter"); // 指定过滤器名称
        registration.setFilter(new LogFilter()); // 指定过滤器实现类
        registration.setUrlPatterns(Collections.singleton("/*"));// 指定拦截路径
        registration.addInitParameter("paramName""paramValue");// 指定初始化参数
        registration.setOrder(1);// 指定顺序
        return registration;
    }
}

将服务启动,访问http://localhost:8080/getUsers,看看控制台输出结果。

日志过滤器,request url :/getUsers
收到查询用户的请求

说明过滤器已经正常工作了。

2.3.3、拦截器

拦截器在 web 项目开发过程中也经常会用到,比如用于用户权限的拦截等等。拦截器属于 SpringMVC 自带的功能,因此 SpringBoot 默认就支持,实现方式也很简单。

首先创建一个拦截器实现类,示例如下。

public class SignInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception 
{
        System.out.println("方法前拦截,request url:" +  request.getRequestURI());
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception 
{
        System.out.println("方法中拦截(不能拦截异常),request url:" +  request.getRequestURI());
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception 
{
        System.out.println("方法后拦截(能拦截异常),request url:" +  request.getRequestURI());
    }
}

然后,将拦截器注册到拦截器链中。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 将SignInterceptor添加到拦截器链中,并且指定拦截路径和拦截顺序
        registry.addInterceptor(new SignInterceptor()).addPathPatterns("/*").order(1);
    }
}

将服务启动,访问http://localhost:8080/getUsers,看看控制台输出结果。

方法前拦截,request url:/getUsers
收到查询用户的请求
方法中拦截(不能拦截异常),request url:/getUsers
方法后拦截(能拦截异常),request url:/getUsers

可以发现,过滤器的执行顺序在拦截器之前。

其中拦截器中postHandle()afterCompletion()方法,都可以实现对接口执行后进行拦截,两者不同点在于:

  • postHandle()方法无法拦截异常;
  • afterCompletion()方法可以拦截异常;

可以新增一个getUsersError()方法,增加运行时异常。

@GetMapping("/getUsersError")
public List<User> getUsersError() {
    // 查询所有的用户
    System.out.println("收到查询用户的请求");
    if(1==1){
        throw new NullPointerException("异常测试");
    }
    List<User> users =  userService.getAll();
    return users;
}

再次请求访问http://localhost:8080/getUsersError,控制台输出结果如下。

方法前拦截,request url:/getUsersError
收到查询用户的请求
方法后拦截,request url:/getUsersError

当出现异常时,可见postHandle()方法,没有被执行。

2.3.4、aop 代理

aop 动态代理也是 web 项目开发过程中常用的功能特性,熟悉 Spring 的同学可能知道,Spring 的动态代理技术使用了 aspectj 框架的注解来实现切面技术,因此在使用的时候,需要添加相关的依赖包。

首先在pom.xml文件中添加 aspectj 依赖包,示例如下。

<!--添加 aspectj 依赖包 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

编写动态代理类,代理com.example.springboot.web包下所有类的public方法。

@Order(1)
@Component
@Aspect
public class ControllerAspect {

    /***
     * 定义切入点
     */

    @Pointcut("execution(public * com.example.springboot.web..*.*(..))")
    public void methodAdvice(){}

    /**
     * 方法调用前通知
     */

    @Before(value = "methodAdvice()")
    public void before(JoinPoint joinPoint){
        System.out.println("代理-> 来自Before通知,方法名称:" +  joinPoint.getSignature().getName());
    }

    /**
     * 方法调用后通知
     */

    @After(value = "methodAdvice()")
    public void after(JoinPoint joinPoint){
        System.out.println("代理-> 来自After通知,方法名称:" +  joinPoint.getSignature().getName());
    }

    /**
     * 方法调用后通知,方法正常执行后,有返回值,会通知;如果抛异常,不会通知
     */

    @AfterReturning(value = "methodAdvice()", returning = "returnVal")
    public void afterReturning(JoinPoint joinPoint,Object returnVal){
        System.out.println("代理-> 来自AfterReturning通知,方法名称:" +  joinPoint.getSignature().getName() + ",返回值:" + returnVal.toString());
    }

    /**
     * 方法环绕通知
     */

    @Around(value = "methodAdvice()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("代理-> 来自Around环绕前置通知,方法名称:" +  joinPoint.getSignature().getName());
        Object returnValue = joinPoint.proceed();
        System.out.println("代理-> 来自Around环绕后置通知,方法名称:" +  joinPoint.getSignature().getName());
        return returnValue;
    }


    /**
     * 抛出异常通知
     */

    @AfterThrowing(value = "methodAdvice()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
        System.out.println("代理-> 来自AfterThrowing通知,方法名称:" +  joinPoint.getSignature().getName() + ",错误信息:"   + ex.getMessage());
    }
}

将服务启动,访问http://localhost:8080/getUsers,控制台输出结果如下。

代理-> 来自Around环绕前置通知,方法名称:getUsers
代理-> 来自Before通知,方法名称:getUsers
收到查询用户的请求
代理-> 来自Around环绕后置通知,方法名称:getUsers
代理-> 来自After通知,方法名称:getUsers
代理-> 来自AfterReturning通知,方法名称:getUsers,返回值:[User{id=1, name='张三', age=20}, User{id=2, name='李四', age=21}, User{id=3, name='王五', age=22}]

访问http://localhost:8080/getUsersError,控制台输出结果如下。

代理-> 来自Around环绕前置通知,方法名称:getUsersError
代理-> 来自Before通知,方法名称:getUsersError
收到查询用户的请求
代理-> 来自After通知,方法名称:getUsersError
代理-> 来自AfterThrowing通知,方法名称:getUsersError,错误信息:异常测试

可以很清晰的看到,当出现异常时AfterReturning()通知方法和Around环绕后置通知方法都不会执行,异常信息会进入到AfterThrowing() 通知方法中。

2.3.5、异常处理

Spring Boot 对异常处理也做了很多的支持,开发者可以通过@ExceptionHandler注解来全局代理异常信息,实现方式也很简单。

编写一个全局异常处理类,示例如下。

@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理Exception异常
     * @param ex
     * @return
     */

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Map exceptionHandler(Exception ex)
{
        Map<String,Object> errorMap = new HashMap<>();
        errorMap.put("code","500");
        errorMap.put("message",ex.getMessage());
        return errorMap;
    }
}

将服务启动,访问http://localhost:8080/getUsersError,控制台输出结果如下。

可以看到,异常请求被成功接管。

03、小结

本文主要围绕利用 SpringBoot 框架技术,开发一个传统的 web 项目时需要用到的技术点进行一次简单的知识总结,内容难免有所遗漏,如果有描述不对的地方,欢迎留言指出!

可能有的同学会提出这样的疑问,对于 web 特性中的过滤器、拦截器、aop 代理,如果目标的都是同一个方法,他们的执行顺序怎样的?

在实测过程中,从日志打印来看,执行顺序是过滤器 > 拦截器 > aop 代理,如果有多个相同类型的拦截器,依照顺序依次拦截。

04、参考

1、https://springdoc.cn/spring-boot/index.html

写到最后

最后感谢各位的阅读,原创不易,如果觉得文章写的不错,欢迎大家转发,点击【在看】让更多的人看到,谢谢大家的支持!


推荐阅读

继续滑动看下一个
潘志的研发笔记
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存